package sk.stuba.fiit.perconik.activity.listeners.java.dom;
import java.util.Iterator;
import java.util.List;
import com.google.common.base.Function;
import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import difflib.Delta;
import difflib.DiffUtils;
import difflib.Patch;
import sk.stuba.fiit.perconik.core.java.dom.MatchingNode;
import sk.stuba.fiit.perconik.core.java.dom.difference.NodeDeltaSet;
import sk.stuba.fiit.perconik.core.java.dom.difference.NodeDifferencer;
import sk.stuba.fiit.perconik.eclipse.jdt.core.dom.NodeType;
import sk.stuba.fiit.perconik.eclipse.jdt.core.dom.TreeApiLevel;
import static java.lang.String.format;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newLinkedList;
import static sk.stuba.fiit.perconik.core.java.dom.MatchingNode.unwrap;
import static sk.stuba.fiit.perconik.core.java.dom.MatchingNode.wrap;
// TODO extract abstract class as public API, whole isSimilar functionality can be in an ASTMatcher
final class CompilationUnitDifferencer<N extends ASTNode> implements NodeDifferencer<CompilationUnit, N> {
private static final ASTMatcher matcher = new ASTMatcher(true);
private final Function<? super CompilationUnit, ? extends Iterable<? extends N>> collector;
private NodeDeltaSet.Builder<N> builder;
CompilationUnitDifferencer(final Function<? super CompilationUnit, ? extends Iterable<? extends N>> collector) {
this.collector = checkNotNull(collector);
}
public NodeDeltaSet<N> difference(final CompilationUnit original, final CompilationUnit revised) {
this.builder = NodeDeltaSet.builder();
if (original != null || revised != null) {
Iterable<? extends N> originalNodes = this.collector.apply(original);
Iterable<? extends N> revisedNodes = this.collector.apply(revised);
this.compute(originalNodes, revisedNodes);
}
return this.builder.build();
}
private void compute(final Iterable<? extends N> original, final Iterable<? extends N> revised) {
final Patch<MatchingNode<N>> patch = DiffUtils.diff(wrap(original), wrap(revised));
for (final Delta<MatchingNode<N>> delta: patch.getDeltas()) {
switch (delta.getType()) {
case DELETE:
for (N node: unwrap(delta.getOriginal().getLines())) {
this.builder.delete(node);
}
break;
case INSERT:
for (N node: unwrap(delta.getRevised().getLines())) {
this.builder.add(node);
}
break;
case CHANGE:
List<N> originalNodes = unwrap(delta.getOriginal().getLines());
List<N> revisedNodes = unwrap(delta.getRevised().getLines());
List<N> unmatchedOriginalNodes = newLinkedList();
main: for (N originalNode: originalNodes) {
int revisedNodesSize = revisedNodes.size();
for (int k = 0; k < revisedNodesSize; k ++) {
N revisedNode = revisedNodes.get(k);
if (isSimilar(originalNode, revisedNode)) {
this.builder.modify(originalNode, revisedNode);
revisedNodes.remove(k);
continue main;
}
}
unmatchedOriginalNodes.add(originalNode);
}
for (N originalNode: unmatchedOriginalNodes) {
this.builder.delete(originalNode);
}
for (N revisedNode: revisedNodes) {
this.builder.add(revisedNode);
}
break;
default:
throw new IllegalStateException(format("Unknown delta type %s", delta.getType()));
}
}
}
// body declaration routers
private static boolean similar(final List<ASTNode> original, final List<ASTNode> revised) {
if (original.size() != revised.size()) {
return false;
}
Iterator<ASTNode> revisedIterator = revised.iterator();
for (ASTNode originalNode: original) {
if (!isSimilar(originalNode, revisedIterator.next())) {
return false;
}
}
return true;
}
private static boolean isSimilar(final ASTNode original, final ASTNode revised) {
if (original instanceof AbstractTypeDeclaration) {
return isSimilar((AbstractTypeDeclaration) original, revised);
}
switch (NodeType.valueOf(original)) {
case ANNOTATION_TYPE_MEMBER_DECLARATION:
return isSimilar((AnnotationTypeMemberDeclaration) original, revised);
case ENUM_CONSTANT_DECLARATION:
return isSimilar((EnumConstantDeclaration) original, revised);
case FIELD_DECLARATION:
return isSimilar((FieldDeclaration) original, revised);
case INITIALIZER:
return isSimilar((Initializer) original, revised);
case METHOD_DECLARATION:
return isSimilar((MethodDeclaration) original, revised);
default:
return false;
}
}
private static boolean isSimilar(final AbstractTypeDeclaration original, final ASTNode revised) {
switch (NodeType.valueOf(original)) {
case ANNOTATION_TYPE_DECLARATION:
return isSimilar((AnnotationTypeDeclaration) original, revised);
case ENUM_DECLARATION:
return isSimilar((EnumDeclaration) original, revised);
case TYPE_DECLARATION:
return isSimilar((TypeDeclaration) original, revised);
default:
return false;
}
}
// abstract type declarations
private static boolean isSimilar(final AnnotationTypeDeclaration original, final ASTNode revised) {
if (!(revised instanceof AnnotationTypeDeclaration)) {
return false;
}
AnnotationTypeDeclaration other = (AnnotationTypeDeclaration) revised;
boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName());
if (nameMatches) {
return true;
}
boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && similar(original.bodyDeclarations(), other.bodyDeclarations());
return restMatches;
}
private static boolean isSimilar(final EnumDeclaration original, final ASTNode revised) {
if (!(revised instanceof EnumDeclaration)) {
return false;
}
EnumDeclaration other = (EnumDeclaration) revised;
boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName());
if (nameMatches) {
return true;
}
boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeListMatch(original.superInterfaceTypes(), other.superInterfaceTypes()) && matcher.safeSubtreeListMatch(original.enumConstants(), other.enumConstants()) && similar(original.bodyDeclarations(), other.bodyDeclarations());
return restMatches;
}
private static boolean isSimilar(final TypeDeclaration original, final ASTNode revised) {
if (!(revised instanceof TypeDeclaration)) {
return false;
}
TypeDeclaration other = (TypeDeclaration) revised;
boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName());
if (nameMatches) {
return true;
}
boolean restMatches = true;
switch (TreeApiLevel.valueOf(original)) {
case JLS3:
case JLS4:
restMatches = restMatches && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeListMatch(original.typeParameters(), other.typeParameters()) && matcher.safeSubtreeMatch(original.getSuperclassType(), other.getSuperclassType()) && matcher.safeSubtreeListMatch(original.superInterfaceTypes(), other.superInterfaceTypes());
break;
default:
throw new UnsupportedOperationException(format("Unsupported tree API level %s", TreeApiLevel.valueOf(original)));
}
restMatches = restMatches && original.isInterface() == other.isInterface() && matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeMatch(original.getName(), other.getName()) && similar(original.bodyDeclarations(), other.bodyDeclarations());
return restMatches;
}
// body declarations except abstract type declarations
private static boolean isSimilar(final AnnotationTypeMemberDeclaration original, final ASTNode revised) {
if (!(revised instanceof AnnotationTypeMemberDeclaration)) {
return false;
}
AnnotationTypeMemberDeclaration other = (AnnotationTypeMemberDeclaration) revised;
boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName());
if (nameMatches) {
return true;
}
boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeMatch(original.getType(), other.getType()) && matcher.safeSubtreeMatch(original.getDefault(), other.getDefault());
return restMatches;
}
private static boolean isSimilar(final EnumConstantDeclaration original, final ASTNode revised) {
if (!(revised instanceof EnumConstantDeclaration)) {
return false;
}
EnumConstantDeclaration other = (EnumConstantDeclaration) revised;
boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName());
if (nameMatches) {
return true;
}
boolean restMatches = matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeListMatch(original.arguments(), other.arguments()) && matcher.safeSubtreeMatch(original.getAnonymousClassDeclaration(), other.getAnonymousClassDeclaration());
return restMatches;
}
private static boolean isSimilar(final FieldDeclaration original, final ASTNode revised) {
if (!(revised instanceof FieldDeclaration)) {
return false;
}
FieldDeclaration other = (FieldDeclaration) revised;
boolean fragmentMatches = false;
for (Object originalFragment: original.fragments()) {
if (!(originalFragment instanceof VariableDeclarationFragment)) {
continue;
}
for (Object otherFragment: other.fragments()) {
if (!(otherFragment instanceof ASTNode)) {
continue;
}
if (isSimilar((VariableDeclarationFragment) originalFragment, (ASTNode) otherFragment)) {
fragmentMatches = true;
break;
}
}
}
if (fragmentMatches) {
return true;
}
boolean restMatches = true;
switch (TreeApiLevel.valueOf(original)) {
case JLS3:
case JLS4:
restMatches = restMatches && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers());
break;
default:
throw new UnsupportedOperationException(format("Unsupported tree API level %s", TreeApiLevel.valueOf(original)));
}
restMatches = restMatches && matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeMatch(original.getType(), other.getType());
return restMatches;
}
@SuppressWarnings("unused")
private static boolean isSimilar(final Initializer original, final ASTNode revised) {
if (!(revised instanceof Initializer)) {
return false;
}
return true;
}
@SuppressWarnings("deprecation")
private static boolean isSimilar(final MethodDeclaration original, final ASTNode revised) {
if (!(revised instanceof MethodDeclaration)) {
return false;
}
MethodDeclaration other = (MethodDeclaration) revised;
boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName());
if (nameMatches) {
return true;
}
boolean restMatches = true;
switch (TreeApiLevel.valueOf(original)) {
case JLS3:
case JLS4:
restMatches = restMatches && matcher.safeSubtreeListMatch(original.modifiers(), other.modifiers()) && matcher.safeSubtreeMatch(original.getReturnType2(), other.getReturnType2()) && matcher.safeSubtreeListMatch(original.typeParameters(), other.typeParameters());
break;
default:
throw new UnsupportedOperationException(format("Unsupported tree API level %s", TreeApiLevel.valueOf(original)));
}
restMatches = restMatches && original.isConstructor() == other.isConstructor() && matcher.safeSubtreeMatch(original.getJavadoc(), other.getJavadoc()) && matcher.safeSubtreeListMatch(original.parameters(), other.parameters()) && original.getExtraDimensions() == other.getExtraDimensions() && matcher.safeSubtreeListMatch(original.thrownExceptions(), other.thrownExceptions()) && matcher.safeSubtreeMatch(original.getBody(), other.getBody());
return restMatches;
}
// body declaration helpers
private static boolean isSimilar(final VariableDeclarationFragment original, final ASTNode revised) {
if (!(revised instanceof VariableDeclarationFragment)) {
return false;
}
VariableDeclarationFragment other = (VariableDeclarationFragment) revised;
boolean nameMatches = matcher.safeSubtreeMatch(original.getName(), other.getName());
if (nameMatches) {
return true;
}
boolean restMatches = original.getExtraDimensions() == other.getExtraDimensions() && matcher.safeSubtreeMatch(original.getInitializer(), other.getInitializer());
return restMatches;
}
}